/*
 * -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.
 *
 * The Original Code is Rhino code, released May 6, 1998.
 *
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation. Portions created by the Initial Developer are Copyright (C)
 * 1997-1999 the Initial Developer. All Rights Reserved.
 *
 * Contributor(s): Patrick Beard Igor Bukanov Norris Boyd Rob Ginda Kurt
 * Westerfeld Matthias Radestock
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * GNU General Public License Version 2 or later (the "GPL"), in which case the
 * provisions of the GPL are applicable instead of those above. If you wish to
 * allow use of your version of this file only under the terms of the GPL and
 * not to allow others to use your version of this file under the MPL, indicate
 * your decision by deleting the provisions above and replacing them with the
 * notice and other provisions required by the GPL. If you do not delete the
 * provisions above, a recipient may use your version of this file under either
 * the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK *****
 */

package org.moyrax.javascript.shell;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.ImporterTopLevel;
import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
import net.sourceforge.htmlunit.corejs.javascript.Script;
import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
import net.sourceforge.htmlunit.corejs.javascript.Synchronizer;
import net.sourceforge.htmlunit.corejs.javascript.Undefined;
import net.sourceforge.htmlunit.corejs.javascript.Wrapper;
import net.sourceforge.htmlunit.corejs.javascript.serialize.ScriptableInputStream;
import net.sourceforge.htmlunit.corejs.javascript.serialize.ScriptableOutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.moyrax.javascript.annotation.GlobalFunction;
import org.moyrax.javascript.tool.ToolErrorReporter;

/**
 * This class provides for sharing functions across multiple threads. This is of
 * particular interest to server applications.
 *
 * @author Norris Boyd
 */
@SuppressWarnings( "all" )
@org.moyrax.javascript.annotation.Script
public class Global extends ImporterTopLevel {
  /** Default logger for this class. */
  private static final Log log = LogFactory.getLog(Global.class);

  static final long serialVersionUID = 4029130780977538005L;

  /**
   * Keeps the whole list of scopes binded to this {@link ScriptableObject}.
   */
  private static final Map<String, Scriptable> scopes =
      new HashMap<String, Scriptable>();

  NativeArray history;
  private InputStream inStream;
  private PrintStream outStream;
  private PrintStream errStream;
  private boolean sealedStdLib = false;
  boolean initialized;
  private QuitAction quitAction;

  private Scriptable wrappedScope;

  public Global() {}

  public Global(final Scriptable aWrappedScope) {
    this.wrappedScope = aWrappedScope;
  }

  /**
   * Occurs when this {@link Scriptable} object is initialized.
   *
   * @param scope Scope that's being initialized.
   */
  public static void init(final Scriptable scope) {
    if (!scopes.containsKey(scope.getClassName())) {
      scopes.put(scope.getClassName(), new Global(scope));
    } else {
      log.debug("Initialization warning", new IllegalArgumentException(
          "The scope is already registered: " + scope.getClassName()));
    }
  }

  /**
   * Set the action to call from quit().
   */
  public void initQuitAction( QuitAction quitAction ) {
    if ( quitAction == null )
      throw new IllegalArgumentException( "quitAction is null" );
    if ( this.quitAction != null )
      throw new IllegalArgumentException( "The method is once-call." );

    this.quitAction = quitAction;
  }

  public void init( ContextFactory factory ) {
    factory.call( new ContextAction() {
      public Object run( Context cx ) {
        init( cx );
        return null;
      }
    } );
  }

  public void init( Context cx ) {
    // Define some global functions particular to the shell. Note
    // that these functions are not part of ECMA.
    initStandardObjects( cx, sealedStdLib );
    String[] names = { "defineClass", "deserialize", "help", "load",
        "loadClass", "print", "quit", "readFile", "readUrl", "runCommand",
        "seal", "serialize", "spawn", "sync", "toint32", "version", };
    defineFunctionProperties( names, Global.class, ScriptableObject.DONTENUM );

    // Set up "environment" in the global scope to provide access to the
    // System environment variables.
    Environment.defineClass( this );
    Environment environment = new Environment( this );
    defineProperty( "environment", environment, ScriptableObject.DONTENUM );

    history = (NativeArray) cx.newArray( this, 0 );
    defineProperty( "history", history, ScriptableObject.DONTENUM );
    initialized = true;
  }

  /**
   * Print a help message.
   *
   * This method is defined as a JavaScript function.
   */
  @GlobalFunction
  public static void help( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    PrintStream out = getInstance( funObj ).getOut();
    out.println( ToolErrorReporter.getMessage( "msg.help" ) );
  }

  /**
   * Print the string values of its arguments.
   *
   * This method is defined as a JavaScript function. Note that its arguments
   * are of the "varargs" form, which allows it to handle an arbitrary number of
   * arguments supplied to the JavaScript function.
   *
   */
  @GlobalFunction
  public static Object print( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    PrintStream out = getInstance( funObj ).getOut();
    for ( int i = 0; i < args.length; i++ ) {
      if ( i > 0 )
        out.print( " " );

      // Convert the arbitrary JavaScript value into a string form.
      String s = Context.toString( args[ i ] );

      out.print( s );
    }
    out.println();
    return Context.getUndefinedValue();
  }

  /**
   * Call embedding-specific quit action passing its argument as int32 exit
   * code.
   *
   * This method is defined as a JavaScript function.
   */
  @GlobalFunction
  public static void quit( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    Global global = getInstance( funObj );
    if ( global.quitAction != null ) {
      int exitCode = ( args.length == 0 ? 0 : ScriptRuntime.toInt32( args[ 0 ] ) );
      global.quitAction.quit( cx, exitCode );
    }
  }

  /**
   * Get and set the language version.
   *
   * This method is defined as a JavaScript function.
   */
  @GlobalFunction
  public static double version( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    double result = cx.getLanguageVersion();
    if ( args.length > 0 ) {
      double d = Context.toNumber( args[ 0 ] );
      cx.setLanguageVersion( (int) d );
    }
    return result;
  }

  /**
   * Load and execute a set of JavaScript source files.
   *
   * This method is defined as a JavaScript function.
   *
   */
  @GlobalFunction
  public static void load( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    for ( int i = 0; i < args.length; i++ ) {
      Main.processFile( cx, thisObj, Context.toString( args[ i ] ) );
    }
  }

  /**
   * Load a Java class that defines a JavaScript object using the conventions
   * outlined in ScriptableObject.defineClass.
   * <p>
   * This method is defined as a JavaScript function.
   *
   * @exception IllegalAccessException if access is not available to a reflected
   *              class member
   * @exception InstantiationException if unable to instantiate the named class
   * @exception InvocationTargetException if an exception is thrown during
   *              execution of methods of the named class
   * @exception ClassDefinitionException if the format of the class causes this
   *              exception in ScriptableObject.defineClass
   * @see org.mozilla.javascript.ScriptableObject#defineClass
   */
  @GlobalFunction
  public static void defineClass( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    Class clazz = getClass( args );
    ScriptableObject.defineClass( thisObj, clazz );
  }

  /**
   * Load and execute a script compiled to a class file.
   * <p>
   * This method is defined as a JavaScript function. When called as a
   * JavaScript function, a single argument is expected. This argument should be
   * the name of a class that implements the Script interface, as will any
   * script compiled by jsc.
   *
   * @exception IllegalAccessException if access is not available to the class
   * @exception InstantiationException if unable to instantiate the named class
   * @exception InvocationTargetException if an exception is thrown during
   *              execution of methods of the named class
   * @see org.mozilla.javascript.ScriptableObject#defineClass
   */
  @GlobalFunction
  public static void loadClass( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    Class clazz = getClass( args );
    if ( !Script.class.isAssignableFrom( clazz ) ) { throw reportRuntimeError( "msg.must.implement.Script" ); }
    Script script = (Script) clazz.newInstance();
    script.exec( cx, thisObj );
  }

  private static Class getClass( Object[] args ) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    if ( args.length == 0 ) { throw reportRuntimeError( "msg.expected.string.arg" ); }
    Object arg0 = args[ 0 ];
    if ( arg0 instanceof Wrapper ) {
      Object wrapped = ( (Wrapper) arg0 ).unwrap();
      if ( wrapped instanceof Class )
        return (Class) wrapped;
    }
    String className = Context.toString( args[ 0 ] );
    try {
      return Class.forName( className );
    }
    catch ( ClassNotFoundException cnfe ) {
      throw reportRuntimeError( "msg.class.not.found", className );
    }
  }

  @GlobalFunction
  public static void serialize( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IOException {
    if ( args.length < 2 ) { throw Context.reportRuntimeError( "Expected an object to serialize and a filename to write "
            + "the serialization to" ); }
    Object obj = args[ 0 ];
    String filename = Context.toString( args[ 1 ] );
    FileOutputStream fos = new FileOutputStream( filename );
    Scriptable scope = ScriptableObject.getTopLevelScope( thisObj );
    ScriptableOutputStream out = new ScriptableOutputStream( fos, scope );
    out.writeObject( obj );
    out.close();
  }

  @GlobalFunction
  public static Object deserialize( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IOException, ClassNotFoundException {
    if ( args.length < 1 ) { throw Context.reportRuntimeError( "Expected a filename to read the serialization from" ); }
    String filename = Context.toString( args[ 0 ] );
    FileInputStream fis = new FileInputStream( filename );
    Scriptable scope = ScriptableObject.getTopLevelScope( thisObj );
    ObjectInputStream in = new ScriptableInputStream( fis, scope );
    Object deserialized = in.readObject();
    in.close();
    return Context.toObject( deserialized, scope );
  }

  /**
   * The spawn function runs a given function or script in a different thread.
   *
   * js> function g() { a = 7; } js> a = 3; 3 js> spawn(g)
   * Thread[Thread-1,5,main] js> a 3
   */
  @GlobalFunction
  public static Object spawn( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    Scriptable scope = funObj.getParentScope();
    Runner runner;
    if ( args.length != 0 && args[ 0 ] instanceof Function ) {
      Object[] newArgs = null;
      if ( args.length > 1 && args[ 1 ] instanceof Scriptable ) {
        newArgs = cx.getElements( (Scriptable) args[ 1 ] );
      }
      if ( newArgs == null ) {
        newArgs = ScriptRuntime.emptyArgs;
      }
      runner = new Runner( scope, (Function) args[ 0 ], newArgs );
    }
    else if ( args.length != 0 && args[ 0 ] instanceof Script ) {
      runner = new Runner( scope, (Script) args[ 0 ] );
    }
    else {
      throw reportRuntimeError( "msg.spawn.args" );
    }
    runner.factory = cx.getFactory();
    Thread thread = new Thread( runner );
    thread.start();
    return thread;
  }

  /**
   * The sync function creates a synchronized function (in the sense of a Java
   * synchronized method) from an existing function. The new function
   * synchronizes on the <code>this</code> object of its invocation. js> var o =
   * { f : sync(function(x) { print("entry");
   * Packages.java.lang.Thread.sleep(x*1000); print("exit"); })}; js>
   * spawn(function() {o.f(5);}); Thread[Thread-0,5,main] entry js>
   * spawn(function() {o.f(5);}); Thread[Thread-1,5,main] js> exit entry exit
   */
  @GlobalFunction
  public static Object sync( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    if ( args.length == 1 && args[ 0 ] instanceof Function ) {
      return new Synchronizer( (Function) args[ 0 ] );
    }
    else {
      throw reportRuntimeError( "msg.sync.args" );
    }
  }

  /**
   * Execute the specified command with the given argument and options as a
   * separate process and return the exit status of the process.
   * <p>
   * Usage:
   *
   * <pre>
   * runCommand(command)
   * runCommand(command, arg1, ..., argN)
   * runCommand(command, arg1, ..., argN, options)
   * </pre>
   *
   * All except the last arguments to runCommand are converted to strings and
   * denote command name and its arguments. If the last argument is a JavaScript
   * object, it is an option object. Otherwise it is converted to string
   * denoting the last argument and options objects assumed to be empty. Te
   * following properties of the option object are processed:
   * <ul>
   * <li><tt>args</tt> - provides an array of additional command arguments
   * <li><tt>env</tt> - explicit environment object. All its enumeratable
   * properties define the corresponding environment variable names.
   * <li><tt>input</tt> - the process input. If it is not java.io.InputStream,
   * it is converted to string and sent to the process as its input. If not
   * specified, no input is provided to the process.
   * <li><tt>output</tt> - the process output instead of java.lang.System.out.
   * If it is not instance of java.io.OutputStream, the process output is read,
   * converted to a string, appended to the output property value converted to
   * string and put as the new value of the output property.
   * <li><tt>err</tt> - the process error output instead of
   * java.lang.System.err. If it is not instance of java.io.OutputStream, the
   * process error output is read, converted to a string, appended to the err
   * property value converted to string and put as the new value of the err
   * property.
   * </ul>
   */
  @GlobalFunction
  public static Object runCommand( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IOException {
    int L = args.length;
    if ( L == 0 || ( L == 1 && args[ 0 ] instanceof Scriptable ) ) { throw reportRuntimeError( "msg.runCommand.bad.args" ); }

    InputStream in = null;
    OutputStream out = null, err = null;
    ByteArrayOutputStream outBytes = null, errBytes = null;
    Object outObj = null, errObj = null;
    String[] environment = null;
    Scriptable params = null;
    Object[] addArgs = null;
    if ( args[ L - 1 ] instanceof Scriptable ) {
      params = (Scriptable) args[ L - 1 ];
      --L;
      Object envObj = ScriptableObject.getProperty( params, "env" );
      if ( envObj != Scriptable.NOT_FOUND ) {
        if ( envObj == null ) {
          environment = new String[ 0 ];
        }
        else {
          if ( !( envObj instanceof Scriptable ) ) { throw reportRuntimeError( "msg.runCommand.bad.env" ); }
          Scriptable envHash = (Scriptable) envObj;
          Object[] ids = ScriptableObject.getPropertyIds( envHash );
          environment = new String[ ids.length ];
          for ( int i = 0; i != ids.length; ++i ) {
            Object keyObj = ids[ i ], val;
            String key;
            if ( keyObj instanceof String ) {
              key = (String) keyObj;
              val = ScriptableObject.getProperty( envHash, key );
            }
            else {
              int ikey = ( (Number) keyObj ).intValue();
              key = Integer.toString( ikey );
              val = ScriptableObject.getProperty( envHash, ikey );
            }
            if ( val == ScriptableObject.NOT_FOUND ) {
              val = Undefined.instance;
            }
            environment[ i ] = key + '=' + ScriptRuntime.toString( val );
          }
        }
      }
      Object inObj = ScriptableObject.getProperty( params, "input" );
      if ( inObj != Scriptable.NOT_FOUND ) {
        in = toInputStream( inObj );
      }
      outObj = ScriptableObject.getProperty( params, "output" );
      if ( outObj != Scriptable.NOT_FOUND ) {
        out = toOutputStream( outObj );
        if ( out == null ) {
          outBytes = new ByteArrayOutputStream();
          out = outBytes;
        }
      }
      errObj = ScriptableObject.getProperty( params, "err" );
      if ( errObj != Scriptable.NOT_FOUND ) {
        err = toOutputStream( errObj );
        if ( err == null ) {
          errBytes = new ByteArrayOutputStream();
          err = errBytes;
        }
      }
      Object addArgsObj = ScriptableObject.getProperty( params, "args" );
      if ( addArgsObj != Scriptable.NOT_FOUND ) {
        Scriptable s = Context.toObject( addArgsObj, getTopLevelScope( thisObj ) );
        addArgs = cx.getElements( s );
      }
    }
    Global global = getInstance( funObj );
    if ( out == null ) {
      out = ( global != null ) ? global.getOut() : System.out;
    }
    if ( err == null ) {
      err = ( global != null ) ? global.getErr() : System.err;
    }
    // If no explicit input stream, do not send any input to process,
    // in particular, do not use System.in to avoid deadlocks
    // when waiting for user input to send to process which is already
    // terminated as it is not always possible to interrupt read method.

    String[] cmd = new String[ ( addArgs == null ) ? L : L + addArgs.length ];
    for ( int i = 0; i != L; ++i ) {
      cmd[ i ] = ScriptRuntime.toString( args[ i ] );
    }
    if ( addArgs != null ) {
      for ( int i = 0; i != addArgs.length; ++i ) {
        cmd[ L + i ] = ScriptRuntime.toString( addArgs[ i ] );
      }
    }

    int exitCode = runProcess( cmd, environment, in, out, err );
    if ( outBytes != null ) {
      String s = ScriptRuntime.toString( outObj ) + outBytes.toString();
      ScriptableObject.putProperty( params, "output", s );
    }
    if ( errBytes != null ) {
      String s = ScriptRuntime.toString( errObj ) + errBytes.toString();
      ScriptableObject.putProperty( params, "err", s );
    }

    return new Integer( exitCode );
  }

  /**
   * The seal function seals all supplied arguments.
   */
  @GlobalFunction
  public static void seal( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    for ( int i = 0; i != args.length; ++i ) {
      Object arg = args[ i ];
      if ( !( arg instanceof ScriptableObject ) || arg == Undefined.instance ) {
        if ( !( arg instanceof Scriptable ) || arg == Undefined.instance ) {
          throw reportRuntimeError( "msg.shell.seal.not.object" );
        }
        else {
          throw reportRuntimeError( "msg.shell.seal.not.scriptable" );
        }
      }
    }

    for ( int i = 0; i != args.length; ++i ) {
      Object arg = args[ i ];
      ( (ScriptableObject) arg ).sealObject();
    }
  }

  /**
   * The readFile reads the given file context and convert it to a string using
   * the specified character coding or default character coding if explicit
   * coding argument is not given.
   * <p>
   * Usage:
   *
   * <pre>
   * readFile(filePath)
   * readFile(filePath, charCoding)
   * </pre>
   *
   * The first form converts file's context to string using the default
   * character coding.
   */
  @GlobalFunction
  public static Object readFile( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IOException {
    if ( args.length == 0 ) { throw reportRuntimeError( "msg.shell.readFile.bad.args" ); }
    String path = ScriptRuntime.toString( args[ 0 ] );
    String charCoding = null;
    if ( args.length >= 2 ) {
      charCoding = ScriptRuntime.toString( args[ 1 ] );
    }

    return readUrl( path, charCoding, true );
  }

  /**
   * The readUrl opens connection to the given URL, read all its data and
   * converts them to a string using the specified character coding or default
   * character coding if explicit coding argument is not given.
   * <p>
   * Usage:
   *
   * <pre>
   * readUrl(url)
   * readUrl(url, charCoding)
   * </pre>
   *
   * The first form converts file's context to string using the default
   * charCoding.
   */
  @GlobalFunction
  public static Object readUrl( Context cx, Scriptable thisObj, Object[] args, Function funObj ) throws IOException {
    if ( args.length == 0 ) { throw reportRuntimeError( "msg.shell.readUrl.bad.args" ); }
    String url = ScriptRuntime.toString( args[ 0 ] );
    String charCoding = null;
    if ( args.length >= 2 ) {
      charCoding = ScriptRuntime.toString( args[ 1 ] );
    }

    return readUrl( url, charCoding, false );
  }

  /**
   * Convert the argumnet to int32 number.
   */
  @GlobalFunction
  public static Object toint32( Context cx, Scriptable thisObj, Object[] args, Function funObj ) {
    Object arg = ( args.length != 0 ? args[ 0 ] : Undefined.instance );
    if ( arg instanceof Integer )
      return arg;
    return ScriptRuntime.wrapInt( ScriptRuntime.toInt32( arg ) );
  }

  public InputStream getIn() {
    return inStream == null ? System.in : inStream;
  }

  public void setIn( InputStream in ) {
    inStream = in;
  }

  public PrintStream getOut() {
    return outStream == null ? System.out : outStream;
  }

  public void setOut( PrintStream out ) {
    outStream = out;
  }

  public PrintStream getErr() {
    return errStream == null ? System.err : errStream;
  }

  public void setErr( PrintStream err ) {
    errStream = err;
  }

  public void setSealedStdLib( boolean value ) {
    sealedStdLib = value;
  }

  private static Global getInstance( Function function ) {
    Scriptable scope = scopes.get(function.getParentScope().getClassName());

    if ( !( scope instanceof Global ) )
      throw reportRuntimeError( "msg.shell.bad.function.scope",
              String.valueOf( scope ) );
    return (Global) scope;
  }

  /**
   * If any of in, out, err is null, the corresponding process stream will be
   * closed immediately, otherwise it will be closed as soon as all data will be
   * read from/written to process
   */
  private static int runProcess( String[] cmd, String[] environment, InputStream in, OutputStream out, OutputStream err ) throws IOException {
    Process p;
    if ( environment == null ) {
      p = Runtime.getRuntime().exec( cmd );
    }
    else {
      p = Runtime.getRuntime().exec( cmd, environment );
    }
    PipeThread inThread = null, errThread = null;
    try {
      InputStream errProcess = null;
      try {
        if ( err != null ) {
          errProcess = p.getErrorStream();
        }
        else {
          p.getErrorStream().close();
        }
        InputStream outProcess = null;
        try {
          if ( out != null ) {
            outProcess = p.getInputStream();
          }
          else {
            p.getInputStream().close();
          }
          OutputStream inProcess = null;
          try {
            if ( in != null ) {
              inProcess = p.getOutputStream();
            }
            else {
              p.getOutputStream().close();
            }

            if ( out != null ) {
              // Read process output on this thread
              if ( err != null ) {
                errThread = new PipeThread( true, errProcess, err );
                errThread.start();
              }
              if ( in != null ) {
                inThread = new PipeThread( false, in, inProcess );
                inThread.start();
              }
              pipe( true, outProcess, out );
            }
            else if ( in != null ) {
              // No output, read process input on this thread
              if ( err != null ) {
                errThread = new PipeThread( true, errProcess, err );
                errThread.start();
              }
              pipe( false, in, inProcess );
              in.close();
            }
            else if ( err != null ) {
              // No output or input, read process err
              // on this thread
              pipe( true, errProcess, err );
              errProcess.close();
              errProcess = null;
            }

            // wait for process completion
            for ( ;; ) {
              try {
                p.waitFor();
                break;
              }
              catch ( InterruptedException ex ) {}
            }

            return p.exitValue();
          }
          finally {
            // pipe will close stream as well, but for reliability
            // duplicate it in any case
            if ( inProcess != null ) {
              inProcess.close();
            }
          }
        }
        finally {
          if ( outProcess != null ) {
            outProcess.close();
          }
        }
      }
      finally {
        if ( errProcess != null ) {
          errProcess.close();
        }
      }
    }
    finally {
      p.destroy();
      if ( inThread != null ) {
        for ( ;; ) {
          try {
            inThread.join();
            break;
          }
          catch ( InterruptedException ex ) {}
        }
      }
      if ( errThread != null ) {
        for ( ;; ) {
          try {
            errThread.join();
            break;
          }
          catch ( InterruptedException ex ) {}
        }
      }
    }
  }

  static void pipe( boolean fromProcess, InputStream from, OutputStream to ) throws IOException {
    try {
      final int SIZE = 4096;
      byte[] buffer = new byte[ SIZE ];
      for ( ;; ) {
        int n;
        if ( !fromProcess ) {
          n = from.read( buffer, 0, SIZE );
        }
        else {
          try {
            n = from.read( buffer, 0, SIZE );
          }
          catch ( IOException ex ) {
            // Ignore exception as it can be cause by closed pipe
            break;
          }
        }
        if ( n < 0 ) {
          break;
        }
        if ( fromProcess ) {
          to.write( buffer, 0, n );
          to.flush();
        }
        else {
          try {
            to.write( buffer, 0, n );
            to.flush();
          }
          catch ( IOException ex ) {
            // Ignore exception as it can be cause by closed pipe
            break;
          }
        }
      }
    }
    finally {
      try {
        if ( fromProcess ) {
          from.close();
        }
        else {
          to.close();
        }
      }
      catch ( IOException ex ) {
        // Ignore errors on close. On Windows JVM may throw invalid
        // refrence exception if process terminates too fast.
      }
    }
  }

  private static InputStream toInputStream( Object value ) throws IOException {
    InputStream is = null;
    String s = null;
    if ( value instanceof Wrapper ) {
      Object unwrapped = ( (Wrapper) value ).unwrap();
      if ( unwrapped instanceof InputStream ) {
        is = (InputStream) unwrapped;
      }
      else if ( unwrapped instanceof byte[] ) {
        is = new ByteArrayInputStream( (byte[]) unwrapped );
      }
      else if ( unwrapped instanceof Reader ) {
        s = readReader( (Reader) unwrapped );
      }
      else if ( unwrapped instanceof char[] ) {
        s = new String( (char[]) unwrapped );
      }
    }
    if ( is == null ) {
      if ( s == null ) {
        s = ScriptRuntime.toString( value );
      }
      is = new ByteArrayInputStream( s.getBytes() );
    }
    return is;
  }

  private static OutputStream toOutputStream( Object value ) {
    OutputStream os = null;
    if ( value instanceof Wrapper ) {
      Object unwrapped = ( (Wrapper) value ).unwrap();
      if ( unwrapped instanceof OutputStream ) {
        os = (OutputStream) unwrapped;
      }
    }
    return os;
  }

  private static String readUrl( String filePath, String charCoding, boolean urlIsFile ) throws IOException {
    int chunkLength;
    InputStream is = null;
    try {
      if ( !urlIsFile ) {
        URL urlObj = new URL( filePath );
        URLConnection uc = urlObj.openConnection();
        is = uc.getInputStream();
        chunkLength = uc.getContentLength();
        if ( chunkLength <= 0 )
          chunkLength = 1024;
        if ( charCoding == null ) {
          String type = uc.getContentType();
          if ( type != null ) {
            charCoding = getCharCodingFromType( type );
          }
        }
      }
      else {
        File f = new File( filePath );

        long length = f.length();
        chunkLength = (int) length;
        if ( chunkLength != length )
          throw new IOException( "Too big file size: " + length );

        if ( chunkLength == 0 ) { return ""; }

        is = new FileInputStream( f );
      }

      Reader r;
      if ( charCoding == null ) {
        r = new InputStreamReader( is );
      }
      else {
        r = new InputStreamReader( is, charCoding );
      }
      return readReader( r, chunkLength );

    }
    finally {
      if ( is != null )
        is.close();
    }
  }

  private static String getCharCodingFromType( String type ) {
    int i = type.indexOf( ';' );
    if ( i >= 0 ) {
      int end = type.length();
      ++i;
      while ( i != end && type.charAt( i ) <= ' ' ) {
        ++i;
      }
      String charset = "charset";
      if ( charset.regionMatches( true, 0, type, i, charset.length() ) ) {
        i += charset.length();
        while ( i != end && type.charAt( i ) <= ' ' ) {
          ++i;
        }
        if ( i != end && type.charAt( i ) == '=' ) {
          ++i;
          while ( i != end && type.charAt( i ) <= ' ' ) {
            ++i;
          }
          if ( i != end ) {
            // i is at the start of non-empty
            // charCoding spec
            while ( type.charAt( end - 1 ) <= ' ' ) {
              --end;
            }
            return type.substring( i, end );
          }
        }
      }
    }
    return null;
  }

  private static String readReader( Reader reader ) throws IOException {
    return readReader( reader, 4096 );
  }

  private static String readReader( Reader reader, int initialBufferSize ) throws IOException {
    char[] buffer = new char[ initialBufferSize ];
    int offset = 0;
    for ( ;; ) {
      int n = reader.read( buffer, offset, buffer.length - offset );
      if ( n < 0 ) {
        break;
      }
      offset += n;
      if ( offset == buffer.length ) {
        char[] tmp = new char[ buffer.length * 2 ];
        System.arraycopy( buffer, 0, tmp, 0, offset );
        buffer = tmp;
      }
    }
    return new String( buffer, 0, offset );
  }

  static RuntimeException reportRuntimeError( String msgId ) {
    String message = ToolErrorReporter.getMessage( msgId );
    return Context.reportRuntimeError( message );
  }

  static RuntimeException reportRuntimeError( String msgId, String msgArg ) {
    String message = ToolErrorReporter.getMessage( msgId, msgArg );
    return Context.reportRuntimeError( message );
  }
}

class Runner implements Runnable, ContextAction {

  Runner( Scriptable scope, Function func, Object[] args ) {
    this.scope = scope;
    f = func;
    this.args = args;
  }

  Runner( Scriptable scope, Script script ) {
    this.scope = scope;
    s = script;
  }

  public void run() {
    factory.call( this );
  }

  public Object run( Context cx ) {
    if ( f != null )
      return f.call( cx, scope, scope, args );
    else
      return s.exec( cx, scope );
  }

  ContextFactory factory;
  private Scriptable scope;
  private Function f;
  private Script s;
  private Object[] args;
}

class PipeThread extends Thread {

  PipeThread( boolean fromProcess, InputStream from, OutputStream to ) {
    setDaemon( true );
    this.fromProcess = fromProcess;
    this.from = from;
    this.to = to;
  }

  public void run() {
    try {
      Global.pipe( fromProcess, from, to );
    }
    catch ( IOException ex ) {
      throw Context.throwAsScriptRuntimeEx( ex );
    }
  }

  private boolean fromProcess;
  private InputStream from;
  private OutputStream to;
}
